(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
        typeof define === 'function' && define.amd ? define(factory) :
            (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.vui365 = factory());
})(this, (function () {
    'use strict';

    /**
     * 全局配置信息
     */
    class EsConfig {
        // 注意：ES6明确规定，Class内部只有静态方法，没有静态属性。所以要把静态属性改为静态方法。ES7 才支持静态属性，可用 babel 编译。
        // 推广渠道标识
        static channelName = "";
        // 应用市场标识 wxmp=微信小程序, app=应用APP, h5=H5应用, huawei,xiaomi,vivo,oppo
        static appMarket = "h5";
        // 登录接口提供商 weixin=App微信登录, wxmp=微信小程序登录,mobile=手机号登录, test=模拟测试登录
        static loginProvider = "mobile";
        // 是否微信小程序环境
        static isEnvOfWeixinMP = false;
        // 是否 APP 环境（HTML5 Plus）
        static isEnvOfPlus = false;
        // 是否 H5 环境
        static isEnvOfH5 = true;
        // 是否显示电池占位区（微信小程序不用，APP和H5需要）
        static showTopStatusBar = true;
        // 登录会话缓存名称
        static loginSessName = "sessinfo"; // token
        // 开发服务器域名（用于在线图片）
        static domain = "http://127.0.0.1:8080/";
        // 开发服务器接口地址
        // static apiBaseUrl="http://mh.lh/index.php";
        static apiBaseUrl = "http://127.0.0.1:8080//index.php";
        // 默认头像图标
        static defaultAvatarIcon = "/static/images/default.png";
        // 支付宝支付名称 alipay=支付宝App支付,alipayh5=支付宝H5支付
        static payNameOfAlipay = "alipayh5";
        // 微信支付名称 wxpay=微信App支付, wxpaymp=微信小程序支付, wxpayh5=微信H5支付
        static payNameOfWxpay = "wxpayh5";
        // 支付间隔时间
        static payInterval = 10;
        // 短信发送间隔时间（秒）
        static smsInterval = 30;
        // 短信测试验证码（超级验证码）
        static smsTestingCaptcha = "0609";
        // 是否使用模拟数据（Mock.js）
        static isMockData = false;

        /**
         * 设置资源URL地址（切换本地路径为远程路径）
         * @example setResourceUrl('/uploads/2021/1119/d3974335bb7fc1ebe065f3c5c0dfa84f.png')
         * @example setResourceUrl('/static/h5app/images/icon.png')
         */
        static setResourceUrl(path) {
            if (path.indexOf("/static/" == 0)) {
                // return path;
            }
            return path.indexOf('http') == 0 ? path : (EsConfig.domain + path);
        }

        /**
         * 返回可用的头像地址
         */
        static getAvatarImage(path) {
            if (!path || path.trim().length == 0) {
                return EsConfig.defaultAvatarIcon;
            }
            return EsConfig.setResourceUrl(path);
        }
    }

    /**
 * 通用工具类
 * @version 1.0.0
 * @method stdmessage(status, message, data = '', code = 0) 返回标准消息对象
 * @method random(min, max=0) 生成随机数
 */
    class EsUtils {

        /**
         * 返回标准消息对象
         * @param {Number} $status 状态 0=失败,1=成功
         * @param {String} message 消息内容
         * @param {Object} data 数据内容
         * @param {Number} code 状态码 200=成功,401=未授权(未登录),403=禁止访问,500=服务器错误
         * @see https://tool.lu/httpcode/
         */
        static stdmessage(status, message, data = '', code = 0) {
            return {
                status: status,
                message: message,
                data: data,
                code: code,
            }
        }
        /**
         * 生成随机数
         * @example random(1,10) 生成 1...10之间的随机数（包括1和10）。random(10) 生成 0...10 之间的随机数
         */
        static random(min, max = 0) {
            if (max == 0) {
                max = min;
                min = 0;
            }
            return Math.round(Math.random() * (max - min)) + min;
        }

        /**
         * 返回数组中一个或多个随机的键
         */
        static arrayRandom(array, number = 1) {
            let idx = Math.floor((Math.random() * array.length));
            let data = [], array2 = [...array];
            // splice() 方法会改变原始数组
            for (let i = 0; i < number; i++) {
                data.push(array2.splice(idx, 1)[0]);
            }
            return number == 1 ? data[0] : data;
        }

        /**
         * 将数组打乱
         * 注意：此方法会改变原始数组！
         */
        static arrayShuffle(array) {
            // sort() 方法会改变原始数组！
            array.sort(() => {
                return Math.random() > 0.5 ? -1 : 1;
            })
            return array;
        }

        static arrayCopy(arr, result) {
            result = result || [];
            for (var i = 0; i < arr.length; i++) {
                if (arr[i] instanceof Array) {
                    result[i] = [];
                    EsUtils.arrayCopy(arr[i], result[i]);
                } else {
                    result[i] = arr[i];
                }
            }
            return result;
        }
        // 获取浏览器参数
        static getQueryString(name, defval = null, url = '') {
            if (!url) {
                url = window.location.search;
            }
            if (!url) {
                return defval;
            }
            // ?id=1&image=awesome.jpg
            var params = url.substring(url.indexOf('?') + 1).split('&'),
                pair = [];
            for (let param of params) {
                pair = param.split('=');
                if (pair[0] == name) {
                    // return pair[1];
                    return decodeURIComponent(pair[1]);
                }
            }
            return defval;
        }
        // 获取cookie参数
        static getCookie(name, defval = null, cookie = '') {
            if (!cookie) {
                cookie = document.cookie;
            }
            if (!cookie) {
                return defval;
            }
            // cookie1=value; cookie2=value; cookie3=value;
            var params = cookie.split(';'),
                pair = [];
            for (let param of params) {
                pair = param.split('=');
                if (pair[0].trim() == name) {
                    return pair[1];
                }
            }
            return defval;
        }
    }

    /**
     * 表单工具类
     * @version 1.0.0
     * @doc http://sochishun.gitee.io/ui365/components/ui365-form/index.html
     */
    class EsForm {
        /**
         * 版本号
         */
        VERSION = '1.0.0'
        /**
         * 选项
         */
        _option = {
            // 是否输出调试信息
            isDebug: false,
        }
        /**
         * Dom元素对象(Form)
         */
        _elForm = null;

        /**
         * 构造方法
         */
        constructor(el, option = {}) {
            if (typeof (el) == 'string') {
                el = document.querySelector(el);
            }
            // Dom 对象不存在
            if (!el) {
                return;
            }
            // 防止重复初始化
            if (this._elForm) {
                return;
            }
            this._elForm = el;
            this._option = Object.assign(this._option, option);
        }

        getInstance() {
            return this._elForm;
        }
        /**
         * 按钮禁用倒计时效果
         */
        countdownButton(elBtn, interval = 3) {
            if (interval < 1) {
                return;
            }
            if (!elBtn.dataset.text) {
                elBtn.dataset.text = elBtn.innerHTML;
            }
            elBtn.innerHTML = elBtn.dataset.text + `(${interval})`;
            elBtn.disabled = true;
            let intSubmitCd = setInterval(() => {
                interval--;
                elBtn.innerHTML = elBtn.dataset.text + `(${interval})`;
                if (interval < 1) {
                    elBtn.innerHTML = elBtn.dataset.text;
                    elBtn.disabled = false;
                    clearInterval(intSubmitCd);
                }
            }, 1000)
        }

        /**
         * 表单客户端校验
         */
        validateForm() {
            let elForm = this._elForm;
            let formData = new FormData(elForm), elInput = null, error = [];
            // 表单校验
            formData.forEach((v, k) => {
                elInput = elForm[k];
                if (elInput instanceof RadioNodeList || elInput.length) {
                    elInput = elInput[0];
                }
                if(!elInput){
                    return true;
                }
                console.log('elInput222',elInput);
                if (elInput.style.display == 'none') {
                    return true;
                }
                elInput.setCustomValidity('');
                if (elInput.validity.valid === false) { // checkValidity() 浏览器尚未支持，只能用 validity.valid
                    elInput.setCustomValidity(elInput.validationMessage);
                } else {
                    error = this.validateInput(elInput, k, v);
                    if (error.length > 0) {
                        elInput.setCustomValidity(error[1]);
                    }
                }
            })
            elForm.reportValidity(); // 狗日的，终于找到显示的方法了！还是 MDN 文档比较齐全
            return elForm.checkValidity();
        }

        /**
         * 正则验证控件值
         */
        validateInput(elInput, k, v) {
            let pattern = elInput.getAttribute('pattern');
            // input data-rule="id_card"
            if (!pattern && elInput.dataset.rule) {
                pattern = elInput.dataset.rule;
            }
            if (pattern) {
                if (!this.validateRule(pattern, v)) {
                    return [k, `${k} 值无效`];
                }
            }
            return [];

        }

        /**
         * 内置验证规则
         */
        validateRule(pattern, v) {
            let flags = 'ig';
            switch (pattern) {
                case 'number':
                    // 数字
                    pattern = '^\\d*$';
                    break;
                case 'int':
                    // 整数
                    pattern = '^0$|^[1-9]\\d*$';
                    break;
                case 'alpha':
                    // 纯字母
                    pattern = '^[A-Za-z]+$';
                    break;
                case 'alpha_num':
                    // 字母和数字
                    pattern = '^[A-Za-z0-9]+$';
                    break;
                case 'alpha_dash':
                    // 字母和数字，下划线_及破折号-
                    pattern = '^[A-Za-z0-9\\-\\_]+$';
                    break;
                case 'email':
                    pattern = 'w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*';
                    // pattern = '^([A-Za-z0-9_\\-\\.])+\\@([A-Za-z0-9_\\-\\.])+\\.([A-Za-z]{2,4})$';
                    break;
                case 'url':
                    pattern = '^((https?|ftp|file):\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$';
                    break;
                case 'username':
                    // 用户名正则，4到16位（字母，数字，下划线，减号）
                    pattern = '^[a-zA-Z0-9_-]{4,16}$';
                    break;
                case 'password':
                    // 密码强度正则，最少6位，包括至少1个大写字母，1个小写字母，1个数字，1个特殊字符
                    pattern = '^.*(?=.{6,})(?=.*\\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*? ]).*$';
                    break;
                case 'mobile':
                    pattern = '^1[3-9]\\d{9}$';
                    break;
                case 'telphone':
                    // 匹配国内电话号码。如 0511-4405222 或 021-87888822
                    // ^(0\\\\d{2}-\\\\d{8}(-\\\\d{1,4})?)|(0\\\\d{3}-\\\\d{7,8}(-\\\\d{1,4})?)$
                    pattern = '^d{3}-d{8}|d{4}-d{7}$';
                    break;
                case 'zip':
                    // 中国邮政编码为6位数字
                    pattern = '^[1-9]d{5}$';
                    break;
                case 'id_card':
                    // 身份证号（18位/15位）
                    pattern = '(^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$)|(^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}$)';
                    break;
                case 'ip4':
                    // d+.d+.d+.d+
                    pattern = '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$';
                    break;
                case 'date':
                    // 验证日期格式为：2020-11-19
                    pattern = '^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$';
                    break;
                case 'time':
                    // 验证时间格式为：09:00
                    pattern = '^(?:(?:[0-2][0-3])|(?:[0-1][0-9])):[0-5][0-9]$';
                    break;
                case 'datetime':
                    // 验证日期时间格式为：2012-01-31 09:00:22
                    pattern = '^(?:19|20)[0-9][0-9]-(?:(?:0[1-9])|(?:1[0-2]))-(?:(?:[0-2][1-9])|(?:[1-3][0-1])) (?:(?:[0-2][0-3])|(?:[0-1][0-9])):[0-5][0-9]:[0-5][0-9]$';
                    break;
                case 'chinese':
                    pattern = '[\\u4E00-\\u9FA5]';
                    flags = 'u';
                    break;
                default:
                    break;
            }
            return new RegExp(pattern, flags).test(v);
        }
    }
    /**
 * 图形验证码组件
 * @description 图形验证码组件
 * @example <canvas id="captcha"></canvas>
 * 示例：H5ImageCaptcha.init(document.querySelector('#captcha'),150,38)
 */
    class H5ImageCaptcha {
        // 版本号
        version = '1.0.0'
        // 默认选项
        _option = {
            // 图片宽度
            width: 150,
            // 图片高度
            height: 38,
            // 字符集
            codeSet: [...'2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY'],
            // 验证码字体
            fontTtf: '微软雅黑',
            // 验证码字体大小(px)
            fontSize: '23px',
            // 验证码过期时间（s）
            expire: 1800,
            // 验证码位数
            length: 4,
            // 是否添加杂点
            useNoise: true,
            // 是否画混淆曲线
            useCurve: true,
            // 背景颜色
            background: 'white',
            // 是否输出调试信息
            isDebug: true,
            // 超级代码
            superCode: 'o01l',
        }
        /**
         * Dom元素对象(canvas)
         */
        _elCanvas = null;
        /**
         * 实例化对象集合
         */
        static _instances = [];
        // 验证码内容
        value = '';
        // 构造方法
        constructor(el, option = {}) {
            if (typeof (el) == 'string') {
                el = document.querySelector(el);
            }
            // Dom 对象不存在
            if (!el) {
                return;
            }
            // 防止重复初始化
            if (this._elTable) {
                return;
            }
            this._elCanvas = el;
            this._option = Object.assign(this._option, option);
            this.initializeComponent(this._elCanvas);
            // 实例对象集合
            H5ImageCaptcha._instances.push({ el: el, instance: this });
        }
        /**
         * 获取指定元素的实例
         */
        static getInstance(el) {
            if (!el) {
                return null;
            }
            for (let row of H5ImageCaptcha._instances) {
                if (row.el === el) {
                    return row.instance;
                }
            }
            return null;
        }
        // 实例化组件
        initializeComponent(el) {
            if (!el) {
                return;
            }
            el.style.cssText = `width:${this._option.width}px; height:${this._option.height}px; border:solid 1px #CCC;`;
            let characters = [];
            this.draw(el, characters);
            this.log(characters);
            el.onclick = () => {
                this.draw(el, characters);
                this.log(characters);
            }
            this.value = characters.join('').trim();
            return characters;
        }
        /**
         * 刷新验证码
         */
        refresh() {
            this.log('refresh');
            this.initializeComponent(this._elCanvas);
        }
        /**
         * 检测验证码是否正确
         * @param {String} text 验证码
         * @param {Boolean} ignoreCase 是否忽略大小写
         */
        check(text, ignoreCase = true) {
            if (this._option.isDebug && text == this._option.superCode) {
                return true;
            }
            let result = false;
            if (ignoreCase) {
                result = text.length > 0 && text.toLowerCase() == this.value.toLowerCase();
            } else {
                result = text.length > 0 && text == this.value;
            }
            this.log('text=', text, '&value=', this.value, '&result=', result);
            return result;
        }
        log(...args) {
            if (this._option.isDebug) {
                console.log('[H5ImageCaptcha] ', ...args);
            }
        }
        // 绘制图形验证码
        draw(el, characters) {
            let width = el.clientWidth;
            let height = el.clientHeight;
            el.width = width;
            el.height = height;
            let context = el.getContext('2d');
            if (this._option.background) {
                //绘制矩形，作背景色
                context.fillStyle = this._option.background;
                //绘制实心矩形
                context.fillRect(0, 0, width, height)
            }
            let codeSet = this._option.codeSet;
            //codeSet = [...'们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高'];
            //codeSet = [...'量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全'];
            //codeSet = [...'组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立'];
            //codeSet = [...'题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复'];
            //codeSet = [...'安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书术'];
            //codeSet = [...'状厂须离再目海交权且儿青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿千胜细影济白格效置推空配刀叶率述今选养德话查差半敌始片施响收华觉备名红续均药标记'];
            let sLength = codeSet.length;
            // 验证码字体样式
            let fontStyle = `bold ${this._option.fontSize} ${this._option.fontTtf}`;
            // 验证码位数
            let len = this._option.length;
            for (let i = 0; i < len; i++) {
                let j = Math.floor(Math.random() * sLength); // 随机索引
                let deg = Math.random() * 30 * Math.PI / 180; // 0-30随机弧度
                let text = codeSet[j]; // 随机字符
                characters[i] = text;
                let x = 10 + i * 20; // x坐标
                let y = 20 + Math.random() * 8; // y坐标
                // 位移，旋转角度，颜色，文字，样式，起始位置
                context.font = fontStyle;
                context.translate(x, y);
                context.rotate(deg);
                context.fillStyle = this.randomColor();
                context.fillText(text, 0, 0);
                // 复位
                context.rotate(-deg);
                context.translate(-x, -y);
            }
            // 添加杂点
            if (this._option.useNoise) {
                for (let i = 0; i <= 30; i++) {
                    context.strokeStyle = this.randomColor();
                    context.beginPath(); // 开始路径
                    let m = Math.random() * width;
                    let n = Math.random() * height;
                    context.moveTo(m, n);
                    context.lineTo(m + 1, n + 1);
                    context.stroke(); // 画出上面定义好的路径
                }
            }

            if (this._option.useCurve) {
                for (let i = 0; i < 8; i++) {
                    context.strokeStyle = this.randomColor();
                    context.beginPath();
                    context.moveTo(Math.random() * width, Math.random() * height);
                    context.lineTo(Math.random() * width, Math.random() * height);
                    context.stroke();
                }
            }
        }
        // 生成随机颜色
        randomColor() {
            let rgb = {
                r: Math.floor(Math.random() * 256),
                g: Math.floor(Math.random() * 256),
                b: Math.floor(Math.random() * 256),
            }
            return `rgb(${rgb.r},${rgb.g},${rgb.b})`;
        }
    }

    /**
     * Bootstrap Dialog 弹窗类
     * @version 1.0.0
     * @requires Bootstrap 5.x
     */
    class BsDialog {

        static getComponent() {
            return {
                name: "BsDialog",
                props: {
                    name: {
                        type: String,
                        default: "index",
                    }
                },
                data() {
                    return {
                    }
                },
                methods: {
                },
                template: ``
            }
        }

        /**
         * 字符串转为Dom对象
         */
        static strToDom(htmlStr) {
            // 方法1: 使用 Fragment。效率高
            let frag = document.createRange().createContextualFragment(htmlStr);
            return frag.firstChild;
            // 方法2: 使用 DOMParser。效率低
            // return new DOMParser().parseFromString(htmlStr,'text/html');
            // 方法3: 使用常规方法
            // let node=document.createElement('DIV');
            // node.innerHTML=htmlStr;
            // return node.childNodes;
        }

        // ==============================================================================
        // 兼容layer
        // ==============================================================================
        static msg(message, time = 3000) {
            // 基于 layer 插件的全局弹窗方法 https://layer.layui.com/
            // layer.msg(message, { time: time });
            BsDialog.showToast({ content: message, delay: time })
        }
        static toast(message, time = 3000) {
            // 基于 layer 插件的全局弹窗方法 https://layer.layui.com/
            // layer.msg(message, { time: time });
            BsDialog.showToast({ content: message, delay: time })
        }
        static alert(message, title = '消息', option = {}) {
            let _default = {
                title: title,
                content: message, // 消息内容
            }
            BsDialog.showAlert(Object.assign(_default, option))
        }
        static confirm(message, fnOk, fnCancel) {
            BsDialog.showConfirm({ content: message }).then((result) => {
                if (result) {
                    fnOk();
                } else {
                    fnCancel();
                }
            })
        }

        // ==============================================================================
        // 快捷方法
        // ==============================================================================
        static toast(message, time = 3000) {
            BsDialog.showToast({ content: message, delay: time })
        }
        // ==============================================================================
        // 常规方法
        // ==============================================================================

        /**
         * 显示 Toast
         * @example BsDialog.showToast();
         */
        static showToast(option = {}) {
            let _default = {
                id: 'toast_' + new Date().getTime(), // 元素ID
                content: 'Hello, world! This is a toast message.', // 消息内容
                delay: 2000, // 延迟关闭时间(毫秒)
                autohide: true, // 是否自动关闭
                theme: 'dark', // 主题样式
                customStyle: '', // 自定义主题样式
                customButtonStyle: '', // 自定义按钮样式
            }
            if (typeof (option) == 'object') {
                option = Object.assign(_default, option);
            } else {
                _default.content = option
                option = _default;
            }
            if (option.customStyle == '') {
                switch (option.theme) {
                    case 'dark':
                        option.customStyle = 'bg-dark text-white border-0';
                        option.customButtonStyle = 'btn-close-white';
                        break;
                    case 'light':
                        option.customStyle = '';
                        option.customButtonStyle = '';
                        break;
                }
            }
            let html = `<div id="${option.id}" class="toast position-fixed top-50 start-50 translate-middle ${option.customStyle}" role="alert" aria-live="assertive" aria-atomic="true">
        <div class="d-flex">
            <div class="toast-body">
                ${option.content}
            </div>
            <button type="button" class="btn-close me-2 m-auto ${option.customButtonStyle}" data-bs-dismiss="toast" aria-label="Close"></button>
        </div>
    </div>`;
            let el = BsDialog.strToDom(html);
            document.body.appendChild(el);
            var toast = new bootstrap.Toast(el, { delay: option.delay, autohide: option.autohide })
            toast.show()
            // 弹窗隐藏后，删除自身DOM
            el.addEventListener('hidden.bs.toast', function () {
                document.body.removeChild(el);
            })
        }
        /**
         * 显示 Toast
         * @example BsDialog.showToast();
         */
        static showAlert(option = {}) {
            let _default = {
                id: 'alert_' + new Date().getTime(), // 元素ID
                title: 'Modal title',
                content: 'Modal body text goes here.', // 消息内容
            }
            option = Object.assign(_default, option);
            let html = `<div class="modal fade">
          <div class="modal-dialog">
            <div class="modal-content">
              <div class="modal-header">
                <h5 class="modal-title">${option.title}</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
              </div>
              <div class="modal-body">
              ${option.content}
              </div>
              <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">确定</button>
              </div>
            </div>
          </div>
        </div>`;
            return new Promise((resolve) => {
                let el = BsDialog.strToDom(html);
                document.body.appendChild(el);
                var modal = new bootstrap.Modal(el)
                modal.show();
                // 弹窗隐藏后，删除自身DOM
                el.addEventListener('hidden.bs.modal', function () {
                    resolve(false);
                    document.body.removeChild(el);
                })
            });
        }
        /**
         * 显示确认框
         * @example if(await BsDialog.showConfirm({title:'警告', content:'确定要删除吗？'})){alert('确定删除')}else{alert('取消删除')}
         */
        static showConfirm(option = {}) {
            let _default = {
                id: 'confirm_' + new Date().getTime(), // 元素ID
                title: 'Modal title',
                content: 'Modal body text goes here.', // 消息内容
            }
            option = Object.assign(_default, option);
            let html = `<div id="${option.id}" class="modal" tabindex="-1">
            <div class="modal-dialog modal-dialog-centered">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">${option.title}</h5>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                    </div>
                    <div class="modal-body">
                        <p>${option.content}</p>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                        <button type="button" class="btn-ok btn btn-primary">确定</button>
                    </div>
                </div>
            </div>
        </div>`;
            return new Promise((resolve) => {
                let el = BsDialog.strToDom(html);
                document.body.appendChild(el);
                var modal = new bootstrap.Modal(el)
                modal.show();
                // 确定按钮点击事件
                el.querySelector('.btn-ok').addEventListener('click', function (evt) {
                    evt.preventDefault();
                    alert('ok');
                    resolve(true);
                    document.body.removeChild(el);
                })
                // 弹窗隐藏后，删除自身DOM
                el.addEventListener('hidden.bs.modal', function () {
                    resolve(false);
                    document.body.removeChild(el);
                })
            })
        }
        /**
         * 显示输入框
         * @example if(await BsDialog.showPrompt({content:'请输入口令：'})=='123'){alert('口令正确')}else{alert('口令错误')}
         */
        static showPrompt(option = {}) {
            let _default = {
                id: 'modal_' + new Date().getTime(), // 元素ID
                title: 'Modal title',
                content: `<div class="mb-3">
                    <label for="recipient-name" class="col-form-label">Recipient:</label>
                    <input type="text" class="form-control" id="recipient-name">
                </div>
                <div class="mb-3">
                    <label for="message-text" class="col-form-label">Message:</label>
                    <textarea class="form-control" id="message-text"></textarea>
                </div>`, // 消息内容
            }
            option = Object.assign(_default, option);
            let html = `<div id="${option.id}" class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"
        aria-labelledby="staticBackdropLabel" aria-hidden="true">
        <div class="modal-dialog modal-dialog-scrollable">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">${option.title}</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <form>
                        ${option.content}
                    </form>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
                    <button type="button" class="btn-ok btn btn-primary">确定</button>
                </div>
            </div>
        </div>
    </div>`;
            let el = BsDialog.strToDom(html);
            document.body.appendChild(el);
            var modal = new bootstrap.Modal(el)
            modal.show();
            // 确定按钮点击事件
            el.querySelector('.btn-ok').addEventListener('click', function (evt) {
                evt.preventDefault();
                alert('ok');
            })
            // 弹窗隐藏后，删除自身DOM
            el.addEventListener('hidden.bs.modal', function () {
                document.body.removeChild(el);
            })
        }
        static showFormDialogByIframe(option = {}) {
            let _default = {
                id: 'modal_' + new Date().getTime(), // 元素ID
                title: 'Modal title',
                url: '', // iframe url
                size: "modal-lg", // modal-xl, modal-lg, modal-sm
                opener: null,
                showFooter:false,
            }
            option = Object.assign(_default, option);
            let footer=``;
            if(option.showFooter){
                footer=`<div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                <button type="button" class="btn btn-primary">保存</button>
            </div>`;
            }
            let html = `<div id="${option.id}" class="modal fade" tabindex="-1">
                <div class="modal-dialog modal-dialog-scrollable modal-dialog-centered modal-fullscreen-sm-down ${option.size}">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h5 class="modal-title">${option.title}</h5>
                            <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                        </div>
                        <div class="modal-body p-0">
                            <iframe src="${option.url}" frameborder="0" scrolling="auto" allowtransparency="true" allowfullscreen></iframe>
                        </div>
                        ${footer}
                    </div>
                </div>
            </div>`;
            let el = BsDialog.strToDom(html);
            document.body.appendChild(el);
            var modal = new bootstrap.Modal(el)
            modal.handleUpdate()
            modal.show();
            // 弹窗隐藏后，删除自身DOM
            el.addEventListener('hidden.bs.modal', function () {
                document.body.removeChild(el);
            })

            // 设置iframe子窗体的父窗体对象
            let elIframe = el.querySelector('iframe');
            if (option.opener) {
                elIframe.contentWindow.opener = option.opener;
            }
            return modal;
            elIframe.onload = function () {
                // 页面就绪后，执行对 opener 窗体的回调函数 option.opener.onSubWindowCallback()
                if (typeof (elIframe.contentWindow.onOpenerWindowCallback) == 'function') {
                    elIframe.contentWindow.onOpenerWindowCallback();
                    // onOpenerWindowCallback() 函数的示例代码
                    // if (window.opener && typeof (window.opener.onSubWindowCallback) == 'function') {
                    //     window.opener.onSubWindowCallback();
                    // }
                }
                // 把 iframe 里面的表单按钮移动到当前对话框底部。
                // 注意：chrome要发布网站同源的才能互相操作，本地测试操作不了iframe。
                // elIframe.contentDocument.querySelector('body').style.width = 'calc(' + elDialog.style.width + ' - 80px)';
                let elFormFooter = elIframe.contentDocument.querySelector('.dialog-footer');
                if (elFormFooter) {
                    let elFooter = el.querySelector('.modal-footer');
                    // 清空弹窗底部所有按钮
                    while (elFooter.hasChildNodes()) {
                        elFooter.removeChild(elFooter.lastChild);
                    }
                    Array.from(elFormFooter.children).forEach(elBtn => {
                        console.log(elBtn);
                        elFooter.appendChild(elBtn);
                    })
                }
            }
        }
    }

    /**
     * CRUD数据表组件
     * @description 包含完整的增删改查功能的数据表组件
     * @property {Array} navData 表头标签栏的数据源
     * @property {Array} columns 数据列的配置数据
     * @property {Array} dataSource 数据表的数据源
     * @property {Number} total 记录总数
     * @property {Number} pageSize 每一页多少行
     * @event {Function} refresh 刷新数据表
     * @event {Function} create 【创建】按钮点击事件
     * @event {Function} edit 【编辑】按钮点击事件
     * @example <data-table
                  :columns="columns"
                  :data-source="list"
                  :page="page"
                  :pageSize="pageSize"
                  :total="total"
                  :pageSizeList="pageSizeList"
                  @change-page-size="onChangePageSize"
                  @paging="onPaging"
                  @create="onEdit"
                  @edit="onEdit"
                  @delete="onDelete"
                  @refresh="onRefresh"
                ></data-table>
     */
    class DataTable {
        static getComponent() {
            return {
                // 组件名称
                name: "DataTable",
                // props 配置
                props: {
                    idField: {
                        type: String,
                        default: "id",
                    },
                    navData: {
                        type: Array,
                        default() {
                            return [];
                        },
                    },
                    columns: {
                        type: Array,
                        default() {
                            return [];
                        },
                    },
                    dataSource: {
                        type: Array,
                        default() {
                            return [];
                        },
                    },
                    total: {
                        type: Number,
                        default: 0,
                    },
                    pageSize: {
                        type: Number,
                        default: 10,
                    },
                    pageSizeList: {
                        type: Array,
                        default() {
                            return [10, 25, 50, 100, 1000];
                        },
                    },
                    pageListCount: {
                        type: Number,
                        default: 6,
                    },
                    enableCopy:{
                        type:Boolean,
                        default:true,
                    },
                    enableDeleteMultip:{
                        type:Boolean,
                        default:true,
                    }
                },
                // emits 配置
                emits: [
                    "refresh",
                    "create",
                    "edit",
                    "delete",
                    "copy",
                    "toggleColumn",
                    "search",
                    "searchAdv",
                    "changePageSize",
                    "paging",
                ],
                // data 配置
                data() {
                    return {
                        page: 1,
                        pageCount: 0,
                        recordCount: this.total,
                        data: this.dataSource,
                        paginationMessage: "显示第 1 到第 10 条记录，总共 317 条记录 每页显示",
                        pageLinkList: [],
                    };
                },
                // watch 配置
                watch: {
                    dataSource(val) {
                        this.data = val;
                    },
                    total(val) {
                        this.recordCount = val;
                    },
                },
                // methods 配置
                methods: {
                    onPaging(value) {
                        switch (value) {
                            case "prev":
                                this.page--;
                                break;
                            case "next":
                                this.page++;
                                break;
                            default:
                                this.page = value;
                                break;
                        }
                        this.$emit("paging", this.page);
                    },
                    getCheckedValues() {
                        let idArr = [];
                        this.data.forEach((row) => {
                            if (row._checked) {
                                idArr.push(row["id"]);
                            }
                        });
                        console.log("ids=", idArr);
                        return idArr;
                    },
                    onToggleColumn($event) {
                        console.log($event.target);
                    },
                    // 批量编辑事件
                    onEditMulti() {
                        let idArr = this.getCheckedValues();
                        this.$emit("edit", idArr, "multip");
                    },
                    // 批量删除事件
                    onDeleteMulti() {
                        let idArr = this.getCheckedValues();
                        this.$emit("delete", idArr, "multip");
                    },
                    onCheck(isChecked, idx) {
                        this.data[idx]._checked = isChecked;
                    },
                    onCheckAll($event) {
                        let isChecked = $event.target.checked;
                        this.data.forEach((row) => {
                            row._checked = isChecked;
                        });
                    },
                    init() {
                        if (this.recordCount < 1) {
                            console.log("[DataTable] 无记录");
                            return;
                        }
                        this.pageCount = Math.ceil(this.recordCount / this.pageSize);
                        // this.iStart = (this.page - 1) * this.pageSize;
                        // this.iEnd = this.iStart + this.dataSource.length;
                        this.paginationMessage = `显示第 ${this.page} 页，总共 ${this.pageCount} 页，总共 ${this.recordCount} 条记录 每页显示`;
                        this.getPageLinkData();
                        console.log("[DataTable] ", this.paginationMessage);
                        console.log("[DataTable] getPageLinkData", this.pageLinkList);
                    },
                    getPageLinkData() {
                        let pageListCount = this.pageListCount - 1,
                            pageCount = this.pageCount,
                            page = this.page;
                        let halfPages = Math.floor(pageListCount / 2),
                            pageStart = 1,
                            pageEnd = 1,
                            data = [];
                        // 直接显示所有页码 1,2,3,4,5 pageCount=5, pageListCount=5
                        if (pageCount <= pageListCount) {
                            pageStart = 1;
                            pageEnd = pageCount;
                            for (let i = pageStart; i <= pageEnd; i++) {
                                data.push({ value: i, active: i == page, disabled: false });
                            }
                            this.pageLinkList = data;
                            return;
                        }
                        // 缩略按钮在右边 1,2,3,4,...100 [3]
                        if (page <= pageListCount - 2) {
                            pageStart = 1;
                            pageEnd = pageListCount - 1;
                            for (let i = pageStart; i <= pageEnd; i++) {
                                data.push({ value: i, active: i == page, disabled: false });
                            }
                            data.push({ value: "...", active: false, disabled: true });
                            data.push({ value: pageCount, active: false, disabled: false });
                            this.pageLinkList = data;
                            return;
                        }
                        // 缩略按钮在左边 1,...,97,98,99,100 [97]
                        if (page > pageCount - pageListCount + 2) {
                            pageStart = pageCount - pageListCount + 2;
                            pageEnd = pageCount;
                            data.push({ value: 1, active: false, disabled: false });
                            data.push({ value: "...", active: false, disabled: true });
                            for (let i = pageStart; i <= pageEnd; i++) {
                                data.push({ value: i, active: i == page, disabled: false });
                            }
                            this.pageLinkList = data;
                            return;
                        }
                        // 缩略按钮在两边 1,...,49,50,51,...100 [4-96]
                        pageStart = page - halfPages + 1;
                        pageEnd = page + halfPages;
                        data.push({ value: 1, active: false, disabled: false });
                        data.push({ value: "...", active: false, disabled: true });
                        for (let i = pageStart; i < page; i++) {
                            data.push({ value: i, active: i == page, disabled: false });
                        }
                        for (let i = page; i < pageEnd; i++) {
                            data.push({ value: i, active: i == page, disabled: false });
                        }
                        data.push({ value: "...", active: false, disabled: true });
                        data.push({ value: pageCount, active: false, disabled: false });
                        this.pageLinkList = data;
                    },
                    initBootstrap() {
                        // dropdown
                        document.querySelectorAll(".dropdown-toggle").forEach((el) => {
                            return new bootstrap.Dropdown(el);
                        });
                        // tabs
                        document.querySelectorAll("#myTab button").forEach((el) => {
                            var tabTrigger = new Tab(el);

                            el.addEventListener("click", function (event) {
                                event.preventDefault();
                                tabTrigger.show();
                            });
                        });
                    },
                },
                // mounted 生命周期方法（挂载元素，获取到DOM节点。调用数据）
                mounted() {
                    this.initBootstrap();
                    this.init();
                },
                template: `<div class="h5dtable-container">
                <ul v-if="navData.length > 0" class="h5dtable-tabs nav nav-tabs mb-2">
                    <li class="nav-item" v-for="(item, idx) of navData" :key="idx">
                        <a :class="'nav-link tab-'+idx+(idx==0?' active':'')" data-toggle="tab" href="#">{{item.text}}</a>
                    </li>
                </ul>
                <!-- /h5dtable-tabs -->
                <div class="h5dtable-header row mb-2">
                    <div class="h5dtable-toolbar col-lg-8 col-md-12">
                        <div class="btn-group btn-group-sm">
                            <button type="button" class="btn-refresh btn btn-sm btn-secondary mr-1" title="刷新"
                                @click="$emit('refresh')">
                                <i class="bi bi-arrow-clockwise"></i>
                            </button>
                            <button type="button" class="btn-create btn btn-sm btn-secondary mr-1" title="新增记录"
                                @click="$emit('create')">
                                <i class="bi bi-plus"></i>
                            </button>
                            <button v-if="false" type="button" class="btn-edit btn btn-sm btn-secondary mr-1" title="批量编辑"
                                @click="onEditMulti">
                                <i class="bi bi-pencil-square"></i>
                            </button>
                            <button v-if="enableDeleteMultip" type="button" class="btn-remove btn btn-sm btn-secondary mr-1" title="批量删除"
                                @click="onDeleteMulti">
                                <i class="bi bi-trash"></i>
                            </button>
                            <div v-if="false" class="btn-colvis btn-group mr-1">
                                <button class="btn btn-sm btn-secondary dropdown-toggle" type="button" data-toggle="dropdown"
                                    title="列控制">
                                    <i class="bi bi-list"></i>
                                </button>
                                <div class="dropdown-menu">
                                    <a class="dropdown-item" v-for="(col, idx) of columns" :key="idx">
                                        <input type="checkbox" :value="col.name" @change="onToggleColumn" />
                                        {{ col.text }}
                                    </a>
                                </div>
                            </div>
                            <button v-if="false" type="button" class="btn-maximize btn btn-sm btn-secondary mr-1" title="最大化">
                                <i class="bi bi-arrows-fullscreen"></i>
                            </button>
                            <button v-if="false" type="button" class="btn-minimize btn btn-sm btn-secondary mr-1" title="最小化">
                                <i class="bi bi-dash-square-dotted"></i>
                            </button>
                        </div>
                    </div>
                    <!-- /h5dtable-toolbar -->
                    <div class="h5dtable-searchbar col-lg-4 col-md-12" data-is-init="1">
                        <div class="input-group input-group-sm d-flex justify-content-end">
                            <input type="search" class="border rounded px-1" placeholder="ID/名称" />
                            <button class="btn btn-sm btn-secondary btn-search" type="button" title="快速搜索"
                                @click="$emit('search')">
                                <i class="bi bi-search"></i>
                            </button>
                            <button v-if="false" class="btn btn-sm btn-secondary btn-advsearch" type="button" title="高级搜索"
                                @click="$emit('searchAdv')">
                                <i class="bi bi-gear"></i> 高级
                            </button>
                        </div>
                    </div>
                    <!-- /h5dtable-searchbar -->
                </div>
                <!-- /h5dtable-header -->
                <div class="h5dtable-body">
                    <div class="h5dtable-table-container">
                        <table class="main-table table table-sm table-bordered table-hover table-striped table-head-fixed m-0">
                            <thead class="table-light">
                                <tr>
                                    <th>
                                        <input type="checkbox" title="全选/取消全选" @change="onCheckAll" />
                                    </th>
                                    <th class="text-nowrap" v-for="(col, idx) of columns" :key="idx">{{col.text}}</th>
                                    <th class="text-nowrap">操作</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr v-for="(row, idx) of data" :key="idx">
                                    <td class="cell-checkbox">
                                        <input type="checkbox" :value="row.id" :title="row.id" :checked="row._checked"
                                            @change="onCheck($event.target.checked, idx)" />
                                    </td>
                                    <td v-for="(col, idxCol) of columns" :key="idxCol">{{ row[col.name] }}</td>
                                    <td class="text-left text-nowrap">
                                        <button class="btn-edit btn btn-sm btn-secondary" title="编辑"
                                            @click="$emit('edit', row['id'], '', row)">
                                            <i class="bi bi-pencil-square"></i>
                                        </button>
                                        <button class="btn-remove btn btn-sm btn-secondary" title="删除"
                                            @click="$emit('delete', row['id'])">
                                            <i class="bi bi-trash"></i>
                                        </button>
                                        <button v-if="enableCopy" class="btn-copy btn btn-sm btn-secondary" title="复制"
                                            @click="$emit('copy', row['id'])">
                                            <i class="bi bi-files"></i>
                                        </button>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                    <div class="h5dtable-grid-container d-flex flex-wrap"></div>
                    <div class="h5dtable-empty-tip text-muted w-100 text-center d-none"
                        style="height: 100px; line-height: 100px">
                        抱歉，没有相关数据！
                    </div>
                </div>
                <!-- /h5dtable-body -->
                <div class="h5dtable-footer row mb-1">
                    <div class="h5dtable-info mt-1 col-lg-6 col-md-12">
                        {{ paginationMessage }}
                        <select class="h5dtable-pagesize-picker border" @change="$emit('changePageSize', $event.target.value)">
                            <option :value="i" :selected="i == pageSize" v-for="i of pageSizeList" :key="i">
                                {{ i }}
                            </option>
                        </select>
                        条记录
                    </div>
                    <!-- /h5dtable-info -->
                    <div class="h5dtable-pagination mt-1 col-lg-6 col-md-12 d-flex justify-content-end">
                        <div class="d-flex align-items-center">
                            <ul class="pagination pagination-sm m-0">
                                <li class="page-item">
                                    <a class="page-link" href="#" @click.prevent="onPaging('prev')">上一页</a>
                                </li>
                                <li :class="'page-item'+(item.active?' active':'')+(item.disabled?' disabled':'')"
                                    v-for="(item, idx) of pageLinkList" :key="idx">
                                    <a class="page-link" href="#" @click.prevent="onPaging(item.value)">{{ item.value }}</a>
                                </li>
                                <li class="page-item">
                                    <a class="page-link" href="#" @click.prevent="onPaging('next')">下一页</a>
                                </li>
                            </ul>
                            <div class="ml-1">
                                <div class="input-group input-group-sm">
                                    <input type="search" class="border px-1" placeholder="页码" :value="page" style="width: 60px"
                                        ref="input_jump_page" />
                                    <button class="btn-gotopage btn btn-secondary" type="button" title="跳转到指定页码"
                                        @click="this.page = this.$refs.input_jump_page.value">
                                        跳转
                                    </button>
                                </div>
                            </div>
                        </div>
                    </div>
                    <!-- /h5dtable-pagination -->
                </div>
                <!-- /h5dtable-footer -->
            </div>`
            }
        }
    }

    class EasyInput {
        static getComponent() {
            return {
                // 组件名称
                name: "EasyInput",
                // props 配置
                props: {
                    component: {
                        type: String,
                        default: "",
                    },
                    name: {
                        type: String,
                        default: "",
                    },
                    value: {
                        type: String,
                        default: "",
                    },
                    placeholder: {
                        type: String,
                        default: "",
                    },
                    dataSource: {
                        type: Array,
                        default() {
                            return [];
                        },
                    },
                    valueField: {
                        type: String,
                        default: "value",
                    },
                    textField: {
                        type: String,
                        default: "text",
                    },
                },
                // data 配置
                data() {
                    return {
                    };
                },
                // methods 配置
                methods: {
                },
                template: `<div>
                <div v-if="component == 'email'" class="input-group input-group-sm">
                    <span class="input-group-text">EMAIL</span>
                    <input class="form-control form-control-sm" :name="name" type="text" placeholder="电子邮箱" />
                </div>
                <div v-else-if="component == 'radio'">
                    <radio-input :name="name" :value="value" :data-source="dataSource"></radio-input>
                </div>
                <div v-else-if="component == 'select'">
                    <select-input :name="name" :value="value" :data-source="dataSource" value-field="valueField"
                        text-field="textField"></select-input>
                </div>
                <div v-else-if="component == 'imageuploader'">
                    <upload-input :name="name" :value="value" param="{imgwidth:150,imgheight:150}" data-file-accept="image/*"
                        data-file-multiplex="true"></upload-input>
                </div>
                <div v-else>
                    <input class="form-control form-control-sm" :name="name" :value="value" type="text"
                        :placeholder="placeholder" />
                </div>
            </div>`
            }
        }
    }

    class CheckboxInput {
        static getComponent() {
            return {
                name: "CheckboxInput",
                props: {
                    name: {
                        type: String,
                        default: "",
                    },
                    textField: {
                        type: String,
                        default: "text",
                    },
                    valueField: {
                        type: String,
                        default: "value",
                    },
                    dataSource: {
                        type: Array,
                        default() {
                            return [];
                        },
                    },
                    value: {
                        type: String,
                        default: "",
                    },
                },
                data() {
                    return {
                        data: this.dataSource,
                    };
                },
                methods: {
                    initCheckedStatus() {
                        let valueData = this.value.split(","),
                            valueField = this.valueField;
                        this.data.forEach((item) => {
                            item._checked = valueData.indexOf(item[valueField].toString()) > -1;
                        });
                    },
                },
                watch: {
                    dataSource(val) {
                        this.data = val;
                    },
                    value() {
                        this.initCheckedStatus()
                    },
                },
                mounted() {
                    this.initCheckedStatus();
                },
                template: `<div>
                <div
                  class="form-check form-check-inline"
                  v-for="(item, idx) in data"
                  :key="idx"
                >
                  <input
                    class="form-check-input"
                    type="checkbox"
                    :name="name"
                    :value="item.value"
                    :checked="item._checked"
                  />
                  <label class="form-check-label">{{ item.text }}</label>
                </div>
              </div>`
            }
        }
    }

    class RadioInput {
        static getComponent() {
            return {
                name: "RadioInput",
                props: {
                    name: {
                        type: String,
                        default: ""
                    },
                    dataSource: {
                        type: Array,
                        default() {
                            return [];
                        }
                    },
                    value: {
                        type: String,
                        default: ""
                    },
                },
                data() {
                    return {
                        data: this.dataSource,
                    };
                },
                watch: {
                    dataSource(val) {
                        // console.log('[RadioInput] data=',val);
                        this.data = val;
                    },
                },
                template: `<div>
                <div class="form-check form-check-inline" v-for="(item, idx) in data" :key="idx">
                    <input class="form-check-input" type="radio" :name="name" :value="item.value"
                        :checked="item.value == value" />
                    <label class="form-check-label">{{ item.text }}</label>
                </div>
            </div>`
            }
        }
    }

    class SelectInput {
        static getComponent() {
            return {
                name: "SelectInput",
                props: {
                    name: {
                        type: String,
                        default: "",
                    },
                    dataSource: {
                        type: Array,
                        default() {
                            return [];
                        },
                    },
                    value: {
                        type: String,
                        default: "",
                    },
                    url: {
                        type: String,
                        default: "",
                    },
                    valueField: {
                        type: String,
                        default: "value",
                    },
                    textField: {
                        type: String,
                        default: "text",
                    },
                    createable: {
                        type: Boolean,
                        default: false,
                    },
                    onCreateHandle: {
                        type: Function,
                    },
                },
                emits: ["create"],
                data() {
                    return {
                        data: this.dataSource,
                    };
                },
                watch: {
                    dataSource(val) {
                        this.data = val;
                    },
                },
                methods: {
                    onCreate() {
                        this.$emits("create");
                    },
                },
                template: `<div class="input-group input-group-sm">
                <select class="form-select form-select-sm" :name="name">
                    <option :value="item[valueField]" :selected="item[valueField] == value" v-for="(item, idx) in data"
                        :key="idx">
                        {{ item[textField] }} {{ item[valueField] }}
                    </option>
                </select>
                <button class="btn-refresh btn btn-sm btn-secondary" title="刷新">
                    <i class="bi bi-arrow-clockwise"></i>
                </button>
                <button v-if="createable" class="btn-create btn btn-sm btn-secondary" @click="onCreate()">
                    添加
                </button>
            </div>`
            }
        }
    }

    class TreeCheckboxInput {
        static getComponent() {
            return {
                name: "TreeCheckboxInput",
                props: {
                    name: {
                        type: String,
                        default: "",
                    },
                    idField: {
                        type: String,
                        default: "id",
                    },
                    pidField: {
                        type: String,
                        default: "pid",
                    },
                    textField: {
                        type: String,
                        default: "name",
                    },
                    valueField: {
                        type: String,
                        default: "id",
                    },
                    dataSource: {
                        type: Array,
                        default() {
                            return [];
                        },
                    },
                    value: {
                        type: String,
                        default: "",
                    },
                },
                data() {
                    return {
                        data: this.dataSource,
                    };
                },
                // watch 配置
                watch: {
                    dataSource(val) {
                        this.data = this.sonsTree(
                            val,
                            0,
                            this.idField,
                            this.pidField,
                            this.textField
                        );
                    },
                    value() {
                        this.initCheckedStatus();
                    },
                },
                methods: {
                    initCheckedStatus() {
                        let valueData = this.value.split(","),
                            valueField = this.valueField;
                        this.data.forEach((item) => {
                            item._checked = valueData.indexOf(item[valueField].toString()) > -1;
                        });
                    },
                    checkAll($event) {
                        let isChecked = $event.target.checked;
                        this.data.forEach((item) => {
                            item._checked = isChecked;
                        });
                    },
                    /**
                     * 从顶级往下找到所有的子子孙孙（子孙树）
                     * @param {array} arr
                     * @param {int} id
                     * @param {string} idField
                     * @param {string} pidField
                     */
                    sonsTree(
                        arr,
                        id = 0,
                        idField = "id",
                        pidField = "pid",
                        textField = "name"
                    ) {
                        let temp = [],
                            level = 0;
                        /**
                         * 设置树形数据的名称列的树形图标的处理方法
                         */
                        let setTreeCell = function (level, pidValue = 0, text = "") {
                            let prefixIcon =
                                `<span class="level-${level}" data-level="${level}" data-pid="${pidValue}">` +
                                (level > 0 ? new Array(level + 1).join("|----") : "") +
                                "</span> ";
                            return prefixIcon + text;
                        };
                        // console.log('==forFn', arr, id, level);
                        let forFn = function (arr, id, level) {
                            for (let item of arr) {
                                // 不能有 id=0 的记录，否则会陷入死循环
                                if (item[idField] == 0) {
                                    continue;
                                }
                                if (item[pidField] == id) {
                                    item._checked = false;
                                    item._level = level;
                                    item[textField] = setTreeCell(level, id, item[textField]);
                                    temp.push(item);
                                    forFn(arr, item[idField], level + 1);
                                }
                            }
                        };
                        forFn(arr, id, level);
                        return temp;
                    },
                    /**
                     * 根据最后一个找到整个家族（家谱树）
                     * @param {array} arr
                     * @param {int} pid
                     * @param {string} idField
                     * @param {string} pidField
                     */
                    familyTree(arr, pid = 0, idField = "id", pidField = "pid") {
                        let temp = [];
                        let forFn = function (arr, pid) {
                            for (let item of arr) {
                                if (item[idField] == pid) {
                                    temp.push(item);
                                    forFn(arr, item[pidField]);
                                }
                            }
                        };
                        forFn(arr, pid);
                        return temp;
                    },
                },
                mounted() {
                    this.initCheckedStatus();
                },
                template: `<div>
                <div class="form-check">
                    <input class="form-check-input" type="checkbox" @change="checkAll($event)" />
                    <label class="form-check-label"> 全选 </label>
                </div>
                <div style="max-height: 300px;overflow-y: auto;border-top: solid 1px #ccc;padding-top: 0.5rem;margin-top: 0.5rem;">
                    <div class="form-check" :value="item[valueField]" :selected="item[valueField] == value"
                        v-for="(item, idx) in data" :key="idx">
                        <input class="form-check-input" type="checkbox" :name="name" :value="item[valueField]"
                            :checked="item._checked" />
                        <label class="form-check-label" v-html="item[textField]"></label>
                    </div>
                </div>
            </div>`
            }
        }
    }

    class TreeSelectInput {
        static getComponent() {
            return {
                name: "TreeSelectInput",
                props: {
                    name: {
                        type: String,
                        default: "",
                    },
                    idField: {
                        type: String,
                        default: "id",
                    },
                    pidField: {
                        type: String,
                        default: "pid",
                    },
                    textField: {
                        type: String,
                        default: "name",
                    },
                    valueField: {
                        type: String,
                        default: "id",
                    },
                    dataSource: {
                        type: Array,
                        default() {
                            return [];
                        },
                    },
                    value: {
                        type: String,
                        default: "",
                    },
                },
                data() {
                    return {
                        data: this.dataSource,
                    };
                },
                // watch 配置
                watch: {
                    dataSource(val) {
                        this.data = this.sonsTree(
                            val,
                            0,
                            this.idField,
                            this.pidField,
                            this.textField
                        );
                    },
                },
                methods: {
                    /**
                     * 从顶级往下找到所有的子子孙孙（子孙树）
                     * @param {array} arr
                     * @param {int} id
                     * @param {string} idField
                     * @param {string} pidField
                     */
                    sonsTree(
                        arr,
                        id = 0,
                        idField = "id",
                        pidField = "pid",
                        textField = "name"
                    ) {
                        let temp = [],
                            level = 0;
                        /**
                         * 设置树形数据的名称列的树形图标的处理方法
                         */
                        let setTreeCell = function (level, pidValue = 0, text = "") {
                            let prefixIcon = (level > 0 ? new Array(level + 1).join("|----") : "")
                            return prefixIcon + text;
                        };
                        // console.log('==forFn', arr, id, level);
                        let forFn = function (arr, id, level) {
                            for (let item of arr) {
                                // 不能有 id=0 的记录，否则会陷入死循环
                                if (item[idField] == 0) {
                                    continue;
                                }
                                if (item[pidField] == id) {
                                    item._checked = false;
                                    item._level = level;
                                    item[textField] = setTreeCell(level, id, item[textField]);
                                    temp.push(item);
                                    forFn(arr, item[idField], level + 1);
                                }
                            }
                        };
                        forFn(arr, id, level);
                        return temp;
                    },
                    /**
                     * 根据最后一个找到整个家族（家谱树）
                     * @param {array} arr
                     * @param {int} pid
                     * @param {string} idField
                     * @param {string} pidField
                     */
                    familyTree(arr, pid = 0, idField = "id", pidField = "pid") {
                        let temp = [];
                        let forFn = function (arr, pid) {
                            for (let item of arr) {
                                if (item[idField] == pid) {
                                    temp.push(item);
                                    forFn(arr, item[pidField]);
                                }
                            }
                        };
                        forFn(arr, pid);
                        return temp;
                    },
                },
                template: `<div class="input-group input-group-sm">
                <select class="form-select form-select-sm" :name="name">
                    <option :value="item[valueField]" :selected="item[valueField] == value" v-for="(item, idx) in data"
                        :key="idx">
                        {{ item[textField] }}
                    </option>
                </select>
                <button class="btn-refresh btn btn-sm btn-secondary" title="刷新">
                    <i class="bi bi-arrow-clockwise"></i>
                </button>
                <button v-if="createable" class="btn-create btn btn-sm btn-secondary" @click="createHandle()">添加</button>
            </div>`
            }
        }
    }

    class BsFormDialog {
        static getComponent() {
            return {
                // 组件名称
                name: "BsFormDialog",
                emits: ["resetHandle", "submitHandle"],
                // data 配置
                data() {
                    return {
                        title: "表单",
                        id: 0,
                        modal: null,
                    };
                },
                // methods 配置
                methods: {
                    show() {
                        this.modal.show();
                    },
                    hide() {
                        this.modal.hide();
                    },
                    onResetHandle() {
                        this.$emit("resetHandle");
                    },
                    onSubmitHandle() {
                        this.$emit("submitHandle");
                    },
                },
                // mounted 生命周期方法
                mounted() {
                    this.modal = new bootstrap.Modal(this.$refs.dialogRef, { backdrop: false });
                    // this.$refs.dialogRef.addEventListener('show.bs.modal', function (event) {
                    //     console.log('show.bs.modal', event.target);
                    //     let offset = Math.abs(window.screenLeft) - window.innerWidth;
                    //     if (offset > 0) {
                    //         alert(offset+'='+Math.abs(window.screenLeft)+'-'+window.innerWidth+' '+Math.round(window.innerWidth / Math.abs(window.screenLeft) * 100));
                    //         event.target.style.left = '-' + (offset / 5) + 'px';
                    //     }
                    // })
                },
                template: `<div ref="dialogRef" class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"
                aria-labelledby="staticBackdropLabel" aria-hidden="true">
                <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
                    <div class="modal-content">
                        <div class="modal-header">
                            <slot name="formHeader">
                                <h5 class="modal-title">{{ title }}</h5>
                                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                            </slot>
                        </div>
                        <div class="modal-body">
                            <form class="form-main form-horizontal" method="POST">
                                <div class="form-body">
                                    <slot></slot>
                                    <!-- /.form-body -->
                                </div>
                            </form>
                        </div>
                        <div class="modal-footer">
                            <slot name="formFooter">
                                <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">
                                    关闭
                                </button>
                                <button type="button" class="btn btn-sm btn-secondary" @click="onResetHandle()">
                                    重置
                                </button>
                                <button type="button" class="btn btn-sm btn-primary" @click="onSubmitHandle()">
                                    保存
                                </button>
                            </slot>
                        </div>
                    </div>
                </div>
            </div>`
            }
        }
    }

    class UploadInput {
        static getComponent() {
            return {
                name: "UploadInput",
                props: {
                    name: {
                        type: String,
                        default: "",
                    },
                    value: {
                        type: String,
                        default: "",
                    },
                    customUpload: {
                        type: Boolean,
                        default: false,
                    },
                    accept: {
                        type: String,
                        default: "image/*",
                    },
                    multiple: {
                        type: Boolean,
                        default: false,
                    },
                },
                emits: ["upload"],
                watch: {
                    value(val) {
                        this.inputValue = val.trim();
                        this.fileList = val.trim().split(",");
                    }
                },
                data() {
                    return {
                        inputValue: this.value.trim(),
                        fileList: this.value.trim().split(","),
                    };
                },
                methods: {
                    setUri(path) {
                        return this.$EsConfig.setServerUri(path);
                    },
                    onFileChange() {
                        let files = this.$refs.file.files;
                        if (files.length < 1) {
                            alert("提示：您没有选择任何文件");
                            return;
                        }
                        console.log("files", files);
                        if (this.customUpload) {
                            this.$emit("upload", files, (val) => {
                                this.uploadCallback(val);
                            });
                            return;
                        }
                        this.onUploadHandle(files, (val) => {
                            this.uploadCallback(val);
                        });
                    },
                    uploadCallback(val) {
                        if (val instanceof Array) {
                            this.inputValue = val.join(",");
                            this.fileList = val;
                        } else {
                            this.inputValue = val;
                            this.fileList = val.trim().split(",");
                        }
                    },
                    onBrowseFile() {
                        this.$refs.file.click();
                    },
                    removeFile(idx) {
                        this.fileList.splice(idx, 1);
                        this.inputValue = this.fileList.join(",");
                    },
                    async onUploadHandle(files, cb) {
                        let formData = new FormData(),
                            isMultiple = files.length > 1;
                        formData.append("is_multiple", isMultiple ? 1 : 0);
                        if (isMultiple) {
                            for (let file of files) {
                                if (typeof file == "object") {
                                    formData.append("file[]", file);
                                }
                            }
                        } else {
                            formData.append("file", files[0]);
                        }
                        const msgInfo = await CommonApi.uploadFile(formData);
                        console.log("[onSubmitHandle] msgInfo=", msgInfo);
                        if (msgInfo.status == 0) {
                            alert(msgInfo.message);
                            return;
                        }
                        if (cb != undefined) {
                            if (msgInfo.data instanceof Array) {
                                let paths = [];
                                msgInfo.data.forEach((row) => {
                                    paths.push(row.data);
                                });
                                cb(paths.join(","));
                            } else {
                                cb(msgInfo.data);
                            }
                        }
                    },
                },
                template: `<div>
                <div class="input-group input-group-sm">
                    <input :name="name" type="text" class="form-control" placeholder="图像路径" :value="inputValue" />
                    <input type="file" ref="file" style="display: none" @change="onFileChange" :multiple="multiple"
                        :accept="accept" />
                    <button class="btn btn-sm btn-secondary btn-browse" type="button" title="浏览并上传文件" @click="onBrowseFile()">
                        <i class="bi bi-folder-open"></i> 浏览
                    </button>
                    <!-- <button
                    class="btn btn-sm btn-secondary btn-picker"
                    type="button"
                    title="从文件管理器选择文件"
                  >
                    <i class="bi bi-cloud"></i> 云文件
                  </button>
                  <button class="btn btn-sm btn-secondary" type="button">
                    <i
                      class="bi bi-question-circle"
                      data-toggle="tooltip"
                      title=""
                    ></i>
                  </button> -->
                </div>
                <div v-if="inputValue.length > 0" class="preview-file-list mb-2">
                    <div class="img-item" v-for="(item, idx) of fileList" :key="idx">
                        <a :href="setUri(item)" target="_blank"><img :src="setUri(item)" alt="" /></a>
                        <a class="btn-remove" title="删除" @click="removeFile(idx)"><i class="bi bi-trash"></i></a>
                    </div>
                    <div class="img-item" style="border: none; padding: 0" v-if="multiple">
                        <a href="#" class="btn btn-outline-secondary btn-add" @click="onBrowseFile"><i
                                class="bi bi-plus"></i></a>
                    </div>
                </div>
            </div>`
            }
        }
    }

    /**
     * 创建自定义 Axios 实例
     * 是否启用mock数据（如果使用 mock 数据，则 url 不能拼接 BaseUrl，否则路径会匹配不上）
     * process.env.VUE_APP_API_BASE_URL // .env 文件主要用于多人协同的团队开发环境，我是一个人全栈，所以不使用 .env 文件，省得每次更改都要重新编译。。。
     * 使用：vui365.EsConfig.apiBaseUrl = 'http://127.0.0.1:8000/'; myAxios=vui365.getMyAxios();
     */
    function getMyAxios() {
        const myAxios = axios.create({
            baseURL: EsConfig.isMockData ? '' : EsConfig.apiBaseUrl,
            timeout: 6000,
        })

        // axios 添加请求拦截
        myAxios.interceptors.request.use(config => {
            const msgInfo = LoginApi.getLoginInfo();
            if (msgInfo.status == 1 && typeof (msgInfo.data) == "string") {
                config.headers.Authorization = msgInfo.data;
            }
            console.log('添加请求拦截', msgInfo, config.headers.Authorization);
            return config;
        })

        // axios 添加输出拦截
        myAxios.interceptors.response.use(response => {
            console.log('添加输出拦截', response);
            if (response.data.status == undefined) {
                return Promise.reject(new Error("服务器返回的数据格式错误"))
            }
            return Promise.resolve(response.data);
        }, error => {
            return Promise.reject(error)
        })
        return myAxios;
    }

    async function stdAxios(url, param, method = "POST") {
        try {
            const msgInfo = await myAxios({
                url: url,
                method: method,
                data: param
            })
            console.log('stdAxios msgInfo=', msgInfo);
            return msgInfo;
        } catch (error) {
            console.log('stdAxios error=', error.message);
            return vui365.EsUtils.stdmessage(0, '网络错误', error);
        }
    }

    // ESM （ES6 模块导出）
    // export {
    //     EsConfig,
    //     EsUtils,
    //     EsForm
    // };

    const index_umd = {
        EsConfig,
        EsUtils,
        EsForm,
        H5ImageCaptcha,
        BsDialog,
        BsFormDialog,
        DataTable,
        EasyInput,
        CheckboxInput,
        RadioInput,
        SelectInput,
        UploadInput,
        getMyAxios,
        stdAxios,
    };
    return index_umd;
}));
